package io.noties.markwon.base.html


import timber.log.Timber
import androidx.annotation.VisibleForTesting
import io.noties.markwon.base.html.jsoup.parser.CharacterReader
import io.noties.markwon.base.html.jsoup.parser.ParseErrorList
import io.noties.markwon.base.html.jsoup.parser.Token
import io.noties.markwon.base.html.jsoup.parser.Tokeniser
import java.lang.Appendable
import java.util.*

/**
 * @since 2.0.0
 */
open class MarkwonHtmlParserFormatImpl internal constructor(
    private val emptyTagReplacement: HtmlEmptyTagFormatReplacement,
    private val trimmingAppender: TrimmingAppender,
    private val blockHandler: BlockHandler
) : MarkwonHtmlParser() {
    private val inlineTags: MutableList<HtmlTagImpl.InlineImpl> = ArrayList(0)
    private var currentBlock: HtmlTagImpl.BlockImpl? = HtmlTagImpl.BlockImpl.root()
    private var preHtmlTag: HtmlTagImpl? = null
    private var isInsidePreTag = false

    // the thing is: we ensure a new line BEFORE block tag
    // but not after, so another tag will be placed on the same line (which is wrong)
    private var previousIsBlock = false
    override fun <T> processFragment(
        output: T,
        htmlFragment: String
    ) where T : Appendable, T : CharSequence {

        // we might want to reuse tokeniser (at least when the same output is involved)
        // as CharacterReader does a bit of initialization (cache etc) as it's
        // primary usage is parsing a document in one run (not parsing _fragments_)
        val tokeniser = Tokeniser(CharacterReader(htmlFragment), ParseErrorList.noTracking())

        while (true) {
            val token = tokeniser.read()
            val tokenType = token!!.type
            if (Token.TokenType.EOF == tokenType) {
                break
            }
            when (tokenType) {
                Token.TokenType.StartTag -> {
                    val startTag = token as Token.StartTag
                    if (isInlineTag(startTag.normalName!!)) {
                        processInlineTagStart(output, startTag)
                    } else {
                        processBlockTagStart(output, startTag)
                    }
                }
                Token.TokenType.EndTag -> {
                    val endTag = token as Token.EndTag
                    if (isInlineTag(endTag.normalName!!)) {
                        processInlineTagEnd(output, endTag)
                    } else {
                        processBlockTagEnd(output, endTag)
                    }
                }
                Token.TokenType.Character -> {
                    processCharacter(output, (token as Token.Character))
                }
                else -> {}
            }

            // do not forget to reset processed token (even if it's not processed)
            token.reset()
        }
    }

    override fun flushInlineTags(documentLength: Int, action: FlushAction<HtmlTag.Inline>) {
        if (inlineTags.size > 0) {
            if (documentLength > HtmlTag.NO_END) {
                for (inline in inlineTags) {
                    inline.closeAt(documentLength)
                }
            }
            action.apply(inlineTags)
            inlineTags.clear()
        } else {
            action.apply(emptyList())
        }
    }

    override fun flushBlockTags(documentLength: Int, action: FlushAction<HtmlTag>) {
        var block = currentBlock?:return
        while (block.parent != null) {
            block = block.parent!!
        }
        if (documentLength > HtmlTag.NO_END) {
            block.closeAt(documentLength)
        }
        val children = block.children()
        if (children.isNotEmpty()) {
            action.apply(children)
        } else {
            action.apply(emptyList())
        }
        currentBlock = HtmlTagImpl.BlockImpl.root()
    }

    override fun reset() {
        inlineTags.clear()
        currentBlock = HtmlTagImpl.BlockImpl.root()
    }

    protected fun <T> processInlineTagStart(
        output: T,
        startTag: Token.StartTag
    ) where T : Appendable, T : CharSequence {
        val name = startTag.normalName
        val inline = HtmlTagImpl.InlineImpl(
            name!!, output.length, extractAttributes(startTag)
        )
        ensureNewLineIfPreviousWasBlock(output)
        if (isVoidTag(name) || startTag.isSelfClosing) {
            val replacement = emptyTagReplacement.replace(inline)
            if (!replacement.isNullOrEmpty()) {
                AppendableUtils.appendQuietly(output, replacement)
            }

            // the thing is: we will keep this inline tag in the list,
            // but in case of void-tag that has no replacement, there will be no
            // possibility to set a span (requires at least one char)
            inline.closeAt(output.length)
        }
        inlineTags.add(inline)
    }

    protected fun <T> processInlineTagEnd(
        output: T,
        endTag: Token.EndTag
    ) where T : Appendable, T : CharSequence {

        // try to find it, if none found -> ignore
        val openInline = findOpenInlineTag(endTag.normalName!!)
        if (openInline != null) {

            // okay, if this tag is empty -> call replacement
            if (isEmpty(output, openInline)) {
                appendEmptyTagReplacement(output, openInline)
            }

            // close open inline tag
            openInline.closeAt(output.length)
            preHtmlTag = openInline
        }
    }

    protected fun <T> processBlockTagStart(
        output: T,
        startTag: Token.StartTag
    ) where T : Appendable, T : CharSequence {
        val blockEntry = currentBlock?:return

        val name = startTag.normalName

        // block tags (all that are NOT inline -> blocks
        // there is only one strong rule -> paragraph cannot contain anything
        // except inline tags
        if (TAG_PARAGRAPH == blockEntry.name) {
            // it must be closed here not matter what we are as here we _assume_
            // that it's a block tag
            blockEntry.closeAt(output.length)
            AppendableUtils.appendQuietly(output, '\n')
            currentBlock = blockEntry.parent
        } else if (TAG_LIST_ITEM == name && TAG_LIST_ITEM == blockEntry.name) {
            // close previous list item if in the same parent
            blockEntry.closeAt(output.length)
            currentBlock = blockEntry.parent
        }
        if (isBlockTag(name!!)) {
            isInsidePreTag = "pre" == name
            ensureNewLine(output)
        } else {
            ensureNewLineIfPreviousWasBlock(output)
        }
        val start = output.length

        val block: HtmlTagImpl.BlockImpl = HtmlTagImpl.BlockImpl.create(name, start, extractAttributes(startTag), currentBlock)

        val isVoid = isVoidTag(name) || startTag.isSelfClosing
        if (isVoid) {
            val replacement = emptyTagReplacement.replace(block)
            if (!replacement.isNullOrEmpty()) {
                AppendableUtils.appendQuietly(output, replacement)
            }
            block.closeAt(output.length)
        }
        appendBlockChild(block.parent!!, block)
        blockHandler.blockStart(output, block, previousIsBlock, preHtmlTag)

        // if not void start filling-in children
        if (!isVoid) {
            currentBlock = block
        }
    }

    protected fun <T> processBlockTagEnd(
        output: T,
        endTag: Token.EndTag
    ) where T : Appendable, T : CharSequence {
        val name = endTag.normalName
        val block = findOpenBlockTag(endTag.normalName!!)
        if (block != null) {
            if ("pre" == name) {
                isInsidePreTag = false
            }

            // okay, if this tag is empty -> call replacement
            if (isEmpty(output, block)) {
                appendEmptyTagReplacement(output, block)
            }
            block.closeAt(output.length)

            // if it's empty -> we do no care about if it's block or not
            if (!block.isEmpty) {
                previousIsBlock = isBlockTag(block.name)
            }
            if (TAG_PARAGRAPH == name) {
                AppendableUtils.appendQuietly(output, '\n')
            }
            currentBlock = block.parent
            preHtmlTag = currentBlock
            blockHandler.blockEnd(output, block)
        }
    }

    protected fun <T> processCharacter(
        output: T,
        character: Token.Character
    ) where T : Appendable, T : CharSequence {

        // there are tags: BUTTON, INPUT, SELECT, SCRIPT, TEXTAREA, STYLE
        // that might have character data that we do not want to display
        if (isInsidePreTag) {
            character.data?.let { AppendableUtils.appendQuietly(output, it) }
        } else {
            ensureNewLineIfPreviousWasBlock(output)
            character.data?.let { trimmingAppender.append(output, it) }
        }
    }

    protected fun appendBlockChild(parent: HtmlTagImpl.BlockImpl, child: HtmlTagImpl.BlockImpl) {
        var children = parent.children
        if (children == null) {
            children = ArrayList(2)
            parent.children = children
        }
        children.add(child)
    }

    protected fun findOpenInlineTag(name: String): HtmlTagImpl.InlineImpl? {
        var inline: HtmlTagImpl.InlineImpl?
        for (i in inlineTags.size - 1 downTo -1 + 1) {
            inline = inlineTags[i]
            if (name == inline.name && inline.end < 0) {
                return inline
            }
        }
        return null
    }

    protected fun findOpenBlockTag(name: String): HtmlTagImpl.BlockImpl? {
        var blockTag = currentBlock
        while (blockTag != null && name != blockTag.name && !blockTag.isClosed) {
            blockTag = blockTag.parent
        }
        return blockTag
    }

    protected fun <T> ensureNewLineIfPreviousWasBlock(output: T) where T : Appendable, T : CharSequence {
        if (previousIsBlock) {
            ensureNewLine(output)
            previousIsBlock = false
        }
    }

    protected fun <T> appendEmptyTagReplacement(
        output: T,
        tag: HtmlTagImpl
    ) where T : Appendable, T : CharSequence {
        val replacement = emptyTagReplacement.replace(tag)
        if (replacement != null) {
            AppendableUtils.appendQuietly(output, replacement)
        }
    }

    class SampleBlockHandler : BlockHandler {
        override fun <T> blockStart(
            out: T,
            node: HtmlTag,
            previousIsBlock: Boolean,
            preNode: HtmlTag?
        ) where T : Appendable, T : CharSequence {
            //Timber.d("blockStart ->${node.name()},preNode ->${preNode?.name()}\tpreviousIsBlock:${previousIsBlock}")
            if (!forceNewLineBlockTag(node.name()) || out.length < 5) return
            val last5Str = out.subSequence(out.length - 5, out.length).toString().replace(" ", "")
            val length = last5Str.length
            var newLineCount = 0
            for (i in 0 until length) {
                if (last5Str[i] == '\n') {
                    newLineCount++
                }
            }
            //Timber.d("blockStart  format [start] last 5 chars: ${ last5Str.replace("\n", "\\n")} ")
            if (newLineCount == 1) {
                forceNewLine(out)
            } else if (newLineCount == 0) {
                forceNewLine(out)
                forceNewLine(out)
            }
            /*Timber.d("blockStart  format [ end ] last 5 chars: %s ",
                out.subSequence(out.length - 5, out.length).toString().replace("\n", "\\n")
            )*/
        }

        override fun <T> blockEnd(out: T, node: HtmlTag) where T : Appendable, T : CharSequence {}
    }

    companion object {
        fun create(blockHandler: BlockHandler): MarkwonHtmlParserFormatImpl {
            return create(HtmlEmptyTagFormatReplacement.create(), blockHandler)
        }

        @JvmOverloads
        fun create(
            inlineTagReplacement: HtmlEmptyTagFormatReplacement = HtmlEmptyTagFormatReplacement.create(),
            blockHandler: BlockHandler = object : BlockHandler {
                override fun <T> blockStart(
                    out: T,
                    node: HtmlTag,
                    previousIsBlock: Boolean,
                    preNode: HtmlTag?
                ) where T : Appendable, T : CharSequence {
                }

                override fun <T> blockEnd(
                    out: T,
                    node: HtmlTag
                ) where T : Appendable, T : CharSequence {
                }
            }
        ): MarkwonHtmlParserFormatImpl {
            return MarkwonHtmlParserFormatImpl(
                inlineTagReplacement,
                TrimmingAppender.Companion.create(),
                blockHandler
            )
        }


        // name here must lower case
        protected fun isInlineTag(name: String): Boolean {
            return INLINE_TAGS.contains(name)
        }

        protected fun isVoidTag(name: String): Boolean {
            return VOID_TAGS.contains(name)
        }

        protected fun isBlockTag(name: String): Boolean {
            return BLOCK_TAGS.contains(name)
        }

        protected fun forceNewLineBlockTag(name: String): Boolean {
            return BLOCK_NEW_LINE_TAGS.contains(name)
        }

        protected fun isParagraphTag(name: String): Boolean {
            return TAG_PARAGRAPH == name
        }

        protected fun <T> ensureNewLine(output: T) where T : Appendable, T : CharSequence {
            val length = output.length
            if (length > 0
                && '\n' != output[length - 1]
            ) {
                AppendableUtils.appendQuietly(output, '\n')
            }
        }

        fun <T> forceNewLine(output: T) where T : Appendable, T : CharSequence {
            AppendableUtils.appendQuietly(output, '\n')
        }

        protected fun extractAttributes(startTag: Token.StartTag): Map<String, String> {
            val map = mutableMapOf<String, String>()
            startTag.attributes?.forEach { attribute ->
                attribute.let {
                    map.put(it.keyEntry.lowercase(), it.valueEntry)
                }
            }
            return map
        }

        protected fun <T> isEmpty(
            output: T,
            tag: HtmlTagImpl
        ): Boolean where T : Appendable, T : CharSequence {
            return tag.start == output.length
        }

        // https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
        @VisibleForTesting
        //var INLINE_TAGS: Set<String>? = null
        //private var VOID_TAGS: Set<String>? = null

        // these are the tags that are considered _block_ ones
        // this parser will ensure that these blocks are started on a new line
        // other tags that are NOT inline are considered as block tags, but won't have new line
        // inserted before them
        // https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
        //private var BLOCK_TAGS: Set<String>? = null
        //private var BLOCK_NEW_LINE_TAGS: Set<String>? = null

         const val TAG_PARAGRAPH = "p"
         const val TAG_LIST_ITEM = "li"

        val INLINE_TAGS = setOf(
        "a", "abbr", "acronym",
        "b", "bdo", "big", "br", "button",
        "cite", "code",
        "dfn",
        "em",
        "i", "img", "input",
        "kbd",
        "label",
        "map",
        "object",
        "q",
        "samp", "script", "select", "small", "span", "strong", "sub", "sup",
        "textarea", "time", "tt",
        "var",
        "audio",
        "video", "iframe"
        )

        val VOID_TAGS = setOf(
        "area",
        "base", "br",
        "col",
        "embed",
        "hr",
        "img", "input",
        "keygen",
        "link",
        "meta",
        "param",
        "source",
        "track",
        "wbr",
        "video", "iframe"
        )

        val BLOCK_TAGS = setOf(
        "address", "article", "aside",
        "blockquote",
        "canvas",
        "dd", "div", "dl", "dt",
        "fieldset", "figcaption", "figure", "footer", "form",
        "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr",
        "li",
        "main",
        "nav", "noscript",
        "ol", "output",
        "p", "pre",
        "section",
        "table", "tfoot",
        "ul"
        )
        val BLOCK_NEW_LINE_TAGS = setOf(
        "address", "article", "aside",
        "canvas",
        "dd", "div", "dl", "dt",
        "fieldset", "figcaption", "figure", "footer", "form",
        "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr",
        "main",
        "nav",
        "output",
        "p", "pre",
        "section",
        "table", "tfoot"
        )
    }
}